use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::video::GLProfile;
use gl::types::*;
use std::ffi::CString;
use std::path::Path;
use cgmath::prelude::*;
use cgmath::{Matrix4, Vector3, Point3, vec3,  Deg, Rad, perspective, Quaternion};
use std::time::Instant;

pub mod glerror;
pub mod shader;
pub mod camera;

const VERTEX_SHADER_LOCATION: &str = "./resources/shaders/vertex.glsl";
const FRAGMENT_SHADER_LOCATION: &str = "./resources/shaders/fragment.glsl";

pub fn run() -> Result<(), String> {
    let sdl_context = sdl2::init()?;
    let video_subsystem = sdl_context.video()?;
    let gl_attr = video_subsystem.gl_attr();
    gl_attr.set_context_profile(GLProfile::Core);
    gl_attr.set_context_version(3, 3);

    let window = video_subsystem
        .window("rust-sdl2 demo: Video", 800, 800)
        .position_centered()
        .opengl()
        .build()
        .map_err(|wbe| wbe.to_string())?;

    let gl_context = window.gl_create_context()?;
    window.gl_make_current(&gl_context)?;

    gl::load_with(|s| video_subsystem.gl_get_proc_address(s) as *const std::os::raw::c_void);

    let texture_image = image::open(&Path::new("./resources/textures/container.jpg")).expect("Failed to load texture");
    let texture_bytes = texture_image.as_bytes();

    let cube_verticies: [f32;180] = [
        -0.5, -0.5, -0.5,  0.0, 0.0,
         0.5, -0.5, -0.5,  1.0, 0.0,
         0.5,  0.5, -0.5,  1.0, 1.0,
         0.5,  0.5, -0.5,  1.0, 1.0,
        -0.5,  0.5, -0.5,  0.0, 1.0,
        -0.5, -0.5, -0.5,  0.0, 0.0,
    
        -0.5, -0.5,  0.5,  0.0, 0.0,
         0.5, -0.5,  0.5,  1.0, 0.0,
         0.5,  0.5,  0.5,  1.0, 1.0,
         0.5,  0.5,  0.5,  1.0, 1.0,
        -0.5,  0.5,  0.5,  0.0, 1.0,
        -0.5, -0.5,  0.5,  0.0, 0.0,
    
        -0.5,  0.5,  0.5,  1.0, 0.0,
        -0.5,  0.5, -0.5,  1.0, 1.0,
        -0.5, -0.5, -0.5,  0.0, 1.0,
        -0.5, -0.5, -0.5,  0.0, 1.0,
        -0.5, -0.5,  0.5,  0.0, 0.0,
        -0.5,  0.5,  0.5,  1.0, 0.0,
    
         0.5,  0.5,  0.5,  1.0, 0.0,
         0.5,  0.5, -0.5,  1.0, 1.0,
         0.5, -0.5, -0.5,  0.0, 1.0,
         0.5, -0.5, -0.5,  0.0, 1.0,
         0.5, -0.5,  0.5,  0.0, 0.0,
         0.5,  0.5,  0.5,  1.0, 0.0,
    
        -0.5, -0.5, -0.5,  0.0, 1.0,
         0.5, -0.5, -0.5,  1.0, 1.0,
         0.5, -0.5,  0.5,  1.0, 0.0,
         0.5, -0.5,  0.5,  1.0, 0.0,
        -0.5, -0.5,  0.5,  0.0, 0.0,
        -0.5, -0.5, -0.5,  0.0, 1.0,
    
        -0.5,  0.5, -0.5,  0.0, 1.0,
         0.5,  0.5, -0.5,  1.0, 1.0,
         0.5,  0.5,  0.5,  1.0, 0.0,
         0.5,  0.5,  0.5,  1.0, 0.0,
        -0.5,  0.5,  0.5,  0.0, 0.0,
        -0.5,  0.5, -0.5,  0.0, 1.0
    ];

    let world_cubes_positions: [Vector3<f32>; 10] = [
        vec3(0.0, 0.0, 0.0),
        vec3(2.0, 5.0, -15.0),
        vec3(-1.5, -2.2, -2.5),
        vec3(-3.8, -2.0, -12.3),
        vec3(2.4, -0.4, -3.5),
        vec3(-1.7, 3.0, -7.5),
        vec3(1.3, -2.0, -2.5),
        vec3(1.5, 2.0, -2.5),
        vec3(1.5, 0.2, -1.5),
        vec3(-1.3, 1.0, -1.5)
    ];

    let mut texture_id: GLuint = 0;
    unsafe { glchk_block!( // TEXTURE BLOCK
        gl::ActiveTexture(gl::TEXTURE0);
        gl::GenTextures(1, &mut texture_id);
        gl::BindTexture(gl::TEXTURE_2D, texture_id);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::REPEAT as i32);	
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::REPEAT as i32);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR_MIPMAP_LINEAR as i32);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
        gl::TexImage2D(
            gl::TEXTURE_2D,        // specifies that target is 2D texture 
            0,                     // the image is base of the mipmap (the largest resolution)
            gl::RGB as i32,
            texture_image.width() as i32,
            texture_image.height() as i32,
            0, // must be 0 
            gl::RGB,
            gl::UNSIGNED_BYTE,
            texture_bytes.as_ptr() as *const std::os::raw::c_void
        );
        gl::GenerateMipmap(gl::TEXTURE_2D);
    );}

    let mut vertex_array_id: GLuint = 0;
    let mut vertex_buffer_id: GLuint = 0;
    unsafe { glchk_block!( // VERTEX BLOCK
        gl::GenVertexArrays(1, &mut vertex_array_id);
        gl::BindVertexArray(vertex_array_id);

        gl::GenBuffers(1, &mut vertex_buffer_id);
        gl::BindBuffer(gl::ARRAY_BUFFER, vertex_buffer_id);
        gl::BufferData(gl::ARRAY_BUFFER, (cube_verticies.len() * std::mem::size_of::<GLfloat>()) as GLsizeiptr, cube_verticies.as_ptr() as *const std::os::raw::c_void, gl::STATIC_DRAW);
        
        gl::VertexAttribPointer(0, 3, gl::FLOAT, gl::FALSE, 5 * std::mem::size_of::<GLfloat>() as GLsizei, std::ptr::null());
        gl::EnableVertexAttribArray(0);
        gl::VertexAttribPointer(1, 2, gl::FLOAT, gl::FALSE, 5 * std::mem::size_of::<GLfloat>() as GLsizei, (3 * std::mem::size_of::<GLfloat>()) as *const std::os::raw::c_void);
        gl::EnableVertexAttribArray(1);
    );}

    let vertex_shader = shader::Shader::new(
        gl::VERTEX_SHADER,
        &CString::new(
            std::fs::read(VERTEX_SHADER_LOCATION).map_err(|e| e.to_string() + &String::from(VERTEX_SHADER_LOCATION))?
        ).unwrap()
    )?;
    let fragment_shader = shader::Shader::new(
        gl::FRAGMENT_SHADER, 
        &CString::new(
            std::fs::read(FRAGMENT_SHADER_LOCATION).map_err(|e| e.to_string() + &String::from(FRAGMENT_SHADER_LOCATION))?
        ).unwrap()
    )?;
    let mut shader_program = shader::ShaderProgram::new()?;
    shader_program.attach_shader(&vertex_shader)?;
    shader_program.attach_shader(&fragment_shader)?;
    shader_program.link_program()?;
    shader_program.add_uniform_name("model");
    shader_program.add_uniform_name("view");
    shader_program.add_uniform_name("projection");

    let projection_mtx4: Matrix4<f32> = perspective(Deg(50.0), 800 as f32 / 800 as f32, 0.1f32, 100.0f32);
    unsafe {
        glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("projection")?, 1, gl::FALSE, projection_mtx4.as_ptr()););
    }

    unsafe { glchk!(gl::Enable(gl::DEPTH_TEST);); } // to prevent fragments from behind to be drawn

    /*let mut camera_position = Point3::new(0.0f32, 0.0f32, 4.0f32);
    let mut camera_direction = Point3::new(0.0f32, 0.0f32, 0.0f32) - camera_position;
    let mut camera_up = vec3(0.0f32, 1.0f32, 0.0f32);
    let mut view_mtx4: Matrix4<f32> = Matrix4::look_at_rh(camera_position, camera_position + camera_direction, camera_up);*/
    let mut camera = camera::Camera::new(Point3::new(0.0f32,  0.0f32, 4.0f32), vec3(0.0f32, 0.0f32, -1.0f32), vec3(0.0f32, 1.0f32, 0.0f32));
    unsafe {
        gl::UseProgram(shader_program.id);
        let view_mtx4 = camera.view_matrix_recalculation();
        glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("view")?, 1, gl::FALSE, view_mtx4.as_ptr()););
    }

    let start = Instant::now();
    let mut event_pump = sdl_context.event_pump()?;
    'running: loop {
        for event in event_pump.poll_iter() {
            match event {
                Event::Quit { .. } => break 'running,
                Event::KeyDown { keycode: Some(keycode), .. } => {
                    match keycode {
                        Keycode::W => {
                            camera.move_along_direction(0.1f32);
                            let view_mtx4 = camera.view_matrix_recalculation();
                            unsafe {
                                glchk!(gl::UseProgram(shader_program.id););
                                glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("view")?, 1, gl::FALSE, view_mtx4.as_ptr()););
                            }
                        },
                        Keycode::A => {
                            camera.rotate_along_up(0.05f32);
                            let view_mtx4 = camera.view_matrix_recalculation();
                            unsafe {
                                glchk!(gl::UseProgram(shader_program.id););
                                glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("view")?, 1, gl::FALSE, view_mtx4.as_ptr()););
                            }
                        }
                        Keycode::S => {
                            camera.move_along_direction(-0.1f32);
                            let view_mtx4 = camera.view_matrix_recalculation();
                            unsafe {
                                glchk!(gl::UseProgram(shader_program.id););
                                glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("view")?, 1, gl::FALSE, view_mtx4.as_ptr()););
                            }
                        }
                        Keycode::D => {
                            camera.rotate_along_up(-0.05f32);
                            let view_mtx4 = camera.view_matrix_recalculation();
                            unsafe {
                                glchk!(gl::UseProgram(shader_program.id););
                                glchk!(gl::UniformMatrix4fv(shader_program.get_uniform_location("view")?, 1, gl::FALSE, view_mtx4.as_ptr()););
                            }
                        }
                        Keycode::Escape => break 'running,
                        _ => {}
                    }
                },
                _ => {}
            }
        }
        unsafe { glchk_block!(
            gl::ClearColor(0.3f32, 0.3f32, 0.3f32, 1.0f32 );
            gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); // don't forget to clear depth buffer!!!
        );}

        // we are about to draw a number of cubes at different locations
        for (i, position) in world_cubes_positions.iter().enumerate() {
            let mut model_mtx4: Matrix4<f32> = Matrix4::from_translation(*position);
            let angle = 20.0 * i as f32;
            model_mtx4 = model_mtx4 * Matrix4::from_axis_angle(vec3(1.0, 0.3, 0.5).normalize(), Rad(angle));
            unsafe { glchk_block!( // DRAW BLOCK
                
                gl::UseProgram(shader_program.id);
                gl::UniformMatrix4fv(shader_program.get_uniform_location("model")?, 1, gl::FALSE, model_mtx4.as_ptr());
                gl::DrawArrays(gl::TRIANGLES, 0, 36);
            );}
        }
            
        window.gl_swap_window();
    }
    unsafe { // FREE RESOURCES BLOCK
        gl::DeleteBuffers(1, &vertex_buffer_id);
        gl::DeleteVertexArrays(1, &vertex_array_id);
    }

    Ok(())
}

pub fn main() {
    match run() {
        Ok(()) => println!("SUCCESS"),
        Err(str) => println!("FAIL {str}"),
    }
}
